using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;

namespace Framework.Network
{
    public abstract class BaseConnection
    {
        public Action<SocketMessage> HandleMessage { get; set; }
        public Socket Socket { get; private set; }
        public int ConnectId { get; set; } = 0;
        public bool IsConnected => Socket != null && Socket.Connected;
        
        private readonly ConcurrentQueue<NetBuffer> sendQueue = new ConcurrentQueue<NetBuffer>();
        private NetUserToken readToken = new NetUserToken();
        private int sending = 0;
        
        protected BaseConnection()
        {
            Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        public abstract void Start();

        public virtual bool Send(NetBuffer data)
        {
            if (data == null || data.Count == 0)
                return false;

            if (!IsConnected)
                return false;
            
            sendQueue.Enqueue(data);

            if (Interlocked.CompareExchange(ref sending, 1, 0) == 0)
            {
                DoSend();
            }
            
            return true;
        }

        private void DoSend()
        {
            if (sendQueue.Count == 0)
            {
                Interlocked.Exchange(ref sending, 0);
                return;
            }

            List<ArraySegment<byte>> buffers = new List<ArraySegment<byte>>();
            while (sendQueue.TryDequeue(out NetBuffer buffer))
            {
                buffers.Add(new ArraySegment<byte>(buffer.Data, buffer.Index, buffer.Count));
            }

            try
            {
                Socket.BeginSend(buffers, SocketFlags.None, OnSend, Socket);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

        private void OnSend(IAsyncResult ar)
        {
            try
            {
                Socket socket = ar.AsyncState as Socket;
                socket.EndSend(ar, out SocketError error);
                DoSend();
            }
            catch (Exception e)
            {
                OnError(e);
            }
        }

        public virtual void Read(bool line, int count, int sessionId)
        {
            
        }
        
        public void Close()
        {
            if (Socket != null)
            {
                if (Socket.Connected)
                    Socket.Shutdown(SocketShutdown.Both);
                Socket.Close();
                Socket = null;
            }
        }

        protected void OnError(Exception e)
        {
            SocketMessage error = new(ConnectId, SocketMessageType.Close, NetUtil.GetErrorMessage(e), 0);
            if (HandleMessage != null)
                HandleMessage(error);
        }

        public void ReadAsync(byte[] buffer, int index, int length, Action<int, Exception> handler, ReadMode mode = ReadMode.FixCount)
        {
            readToken.Clear();
            readToken.Buffer = buffer;
            readToken.Index = index;
            readToken.Length = length;
            readToken.Handler = handler;
            readToken.Mode = mode;
            BeginReceive(readToken);
        }

        private void BeginReceive(NetUserToken token)
        {
            if (!IsConnected)
            {
                token.Handler(token.BytesTransferred, new SocketException((int)SocketError.NotConnected));
                return;
            }

            try
            {
                Socket.BeginReceive(token.Buffer, token.Index + token.BytesTransferred, token.Length, SocketFlags.None, OnReceive, token);
            }
            catch (Exception e)
            {
                token.Handler(token.BytesTransferred, e);
            }
        }

        private void OnReceive(IAsyncResult ar)
        {
            NetUserToken userToken = ar.AsyncState as NetUserToken;

            try
            {
                if (Socket == null)
                {
                    userToken.Handler(userToken.BytesTransferred, new ApplicationException("operattion aborted"));
                    return;
                }
                
                int bytesTransferred = Socket.EndReceive(ar, out SocketError error);
                if (error != SocketError.Success)
                {
                    userToken.Handler(userToken.BytesTransferred, new SocketException((int)error));
                    return;
                }

                if (bytesTransferred == 0)
                {
                    userToken.Handler(userToken.BytesTransferred, new ApplicationException("eof"));
                    return;
                }
                
                userToken.Length -= bytesTransferred;
                userToken.BytesTransferred += bytesTransferred;

                if (userToken.Mode == ReadMode.FixCount)
                {
                    // Since we have not gotten enough bytes for the whole message,
                    // we need to do another receive op.
                    if (userToken.Length != 0)
                    {
                        BeginReceive(userToken);
                        return;
                    }
                }

                userToken.Handler(userToken.BytesTransferred, null);
            }
            catch (Exception e)
            {
                userToken.Handler(userToken.BytesTransferred, e);
            }
        }
    }
}